package fr.alma.domo.model;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;

import fr.alma.domo.util.Log;


@SuppressWarnings("serial")
public class Collaboration extends Resource implements ResourceListener{
	public final static String FAMILY = "fr.alma.domo.model.Collaboration";
	
	private static String COLLABORATION_INSTANCE_TAG = "COLLAB"+UUID.randomUUID().toString();
	private static String COLLABORATION_INDEX_TAG = "SCHEMA_INDEX"+UUID.randomUUID().toString();
	private static String COLLABORATION_SCHEMA_INSTANCE = "SCHEMA_INSTANCE"+UUID.randomUUID().toString();

	final Class<? extends CollaborationSchema> schema;
	
	@Property(type=ServiceResolutionQuery.class)
	public final static String SERVICE_RESOLUTION_NEED = "Collaboration#defect";
	
	private Map<String,List<AbstractService>> instances 
		= Collections.synchronizedMap(new HashMap<String,List<AbstractService>>());
	
	public Collaboration(Class<? extends CollaborationSchema> schema) {
		super(FAMILY);
		this.schema = schema;
	}
	
	/**
	 * Verifie que tous est connecté
	 * @param cs
	 * @throws IllegalCollaborationSchemaException
	 */
	private static final void checkCollaboration(CollaborationSchema cs) throws IllegalCollaborationSchemaException{
		for(Field f : cs.getClass().getDeclaredFields()){
			if((f.getModifiers() & Modifier.STATIC)==0 && f.isAnnotationPresent(CollaborationSchema.Service.class)){
				try {
					f.setAccessible(true);
					if(f.get(cs)==null){
						CollaborationSchema.Service svc = f.getAnnotation(CollaborationSchema.Service.class);
						throw new IllegalCollaborationSchemaException("The service '"+svc.name()+"' is not bound in "+cs.getClass().getName());
					}
				} catch (Throwable e) {
					e.printStackTrace();
				}
			}
		}

		for(Field f : cs.getClass().getFields()){
			if((f.getModifiers() & Modifier.STATIC)==0 && f.isAnnotationPresent(CollaborationSchema.Service.class)){
				try {
					f.setAccessible(true);
					if(f.get(cs)==null){
						CollaborationSchema.Service svc = f.getAnnotation(CollaborationSchema.Service.class);
						throw new IllegalCollaborationSchemaException("The service '"+svc.name()+"' is not bound in "+cs.getClass().getName());
					}
				} catch (Throwable e) {
					e.printStackTrace();
				}
			}
		}
	}
	
	private static final void injectService(CollaborationSchema cs,String name,AbstractService service){
		//On recherche l'attribut ayant l'annotation service et le nom name dans cs
		Field field = null;
		for(Field f : cs.getClass().getDeclaredFields()){
			if((f.getModifiers() & Modifier.STATIC)==0 && f.isAnnotationPresent(CollaborationSchema.Service.class)){
				CollaborationSchema.Service anot = f.getAnnotation(CollaborationSchema.Service.class);
				if(anot.name().equals(name)){
					field = f;
					break;
				}
			}
		}
		
		if(field==null){
			for(Field f : cs.getClass().getFields()){
				if((f.getModifiers() & Modifier.STATIC)==0 && f.isAnnotationPresent(CollaborationSchema.Service.class)){
					CollaborationSchema.Service anot = f.getAnnotation(CollaborationSchema.Service.class);
					if(anot.name().equals(name)){
						field = f;
						break;
					}
				}
			}
		}
		
		//On met a jour le service
		if(field!=null){
			field.setAccessible(true);
			try {
				field.set(cs, service);
			} catch (IllegalArgumentException e) {
				throw new IllegalCollaborationSchemaException(e);
			} catch (IllegalAccessException e) {
				throw new IllegalCollaborationSchemaException(e);
			}
		}
		
		//On a pas trouvé le service
		else{
			throw new IllegalCollaborationSchemaException("No such service '"+name+"' in "+cs.getClass().getName());
		}
	}
	
	private final void cleanResource(Resource r){
		if(r!=null){
			r.removeListener(this);
			if(r.hasAttribute(COLLABORATION_INDEX_TAG))
					r.setAttribute(COLLABORATION_INDEX_TAG, null);
			if(r.hasAttribute(COLLABORATION_INSTANCE_TAG))
					r.setAttribute(COLLABORATION_INSTANCE_TAG, null);
			if(r.hasAttribute(COLLABORATION_SCHEMA_INSTANCE))
					r.setAttribute(COLLABORATION_SCHEMA_INSTANCE, null);
		}
	}
	
	/**
	 * 
	 * @param l
	 * @return
	 */
	public String newInstance(List<AbstractService> l){
		String id = UUID.randomUUID().toString();
		
		CollaborationSchema csi = null;
		try{
			csi = schema.newInstance();
		}
		catch(Throwable t){		
			//FIXME Handle error here
			t.printStackTrace();
		}
		
		for(int idx=0;idx<l.size();++idx){
			AbstractService svc = l.get(idx);
			
			//On rajoute des tags au service pour pouvoir le tracer
			if(!svc.hasAttribute(COLLABORATION_INSTANCE_TAG))
				svc.declareAttribute(COLLABORATION_INSTANCE_TAG, String.class, false,false);
			if(!svc.hasAttribute(COLLABORATION_INDEX_TAG))
				svc.declareAttribute(COLLABORATION_INDEX_TAG, Integer.class, false,false);
			if(!svc.hasAttribute(COLLABORATION_SCHEMA_INSTANCE))
				svc.declareAttribute(COLLABORATION_SCHEMA_INSTANCE,CollaborationSchema.class,false,false);
			svc.setAttribute(COLLABORATION_INSTANCE_TAG,id);
			svc.setAttribute(COLLABORATION_INDEX_TAG, (Integer)idx);
			svc.setAttribute(COLLABORATION_SCHEMA_INSTANCE, csi);
			
			//On injecte le service dans l'instance
			injectService(csi, svc.getAttribute(AbstractService.SERVICE_NAME,String.class), svc);			
		}
		
		//on enregistre l'instance
		instances.put(id, l);

		//On verifie
		checkCollaboration(csi);
				
		//On demarre la negociation
		csi.negociate();

		//On monitor l' état pour pouvoir notifier le gestionnaire d'adaptation
		for(AbstractService svc : l){
			svc.addListener(this);						
		}
		
		return id;
	}

	/**
	 * 
	 * @param instanceId
	 */
	public void deleteInstance(String instanceId){
		List<AbstractService> list = instances.get(instanceId);
		if(list!=null){
			for(AbstractService svc : list){
				cleanResource(svc);
			}
			instances.remove(instanceId);
		}
	}

	@Override
	public void onPropertyChange(final Resource r, String property, Object oldValue, Object newValue) {
		if(r.hasAttribute(COLLABORATION_INSTANCE_TAG) 
				&& r.hasAttribute(COLLABORATION_INDEX_TAG)
				&& r.hasAttribute(COLLABORATION_SCHEMA_INSTANCE)
				&& r.isAttributeFragile(property)
				&& oldValue!=newValue
				&& (oldValue==null || !oldValue.equals(newValue)))
		{			
			Log.i(getClass().getName()+"#onPropertyChange", "property=%s, source=%s[%s]", property,r.getClass().getName(),r.rid);
			
			//On recupère l'identifiant de l'instance
			final String instanceId = r.getAttribute(COLLABORATION_INSTANCE_TAG, String.class);			

			//On recupère l'instance du schema
			final CollaborationSchema schemaInstance = r.getAttribute(COLLABORATION_SCHEMA_INSTANCE,CollaborationSchema.class);
			
			//On recupère indice dans le tableau fournis à l'instanciation
			final Integer index = r.getAttribute(COLLABORATION_INDEX_TAG, Integer.class);
			
			if(instanceId!=null && index!=null && schemaInstance!=null){

				final List<AbstractService> list = instances.get(instanceId);
				if(list!=null){
					
					final String serviceName = r.getAttribute(AbstractService.SERVICE_NAME,String.class);
					//On recupère la description du service à remplacer
					final Description description = schemaInstance.getDescriptionFor(serviceName);
					AbstractService svc = list.get(index);
					//on verifie qu'on a le bon service
					if(svc.rid.equals(r.rid)){

						//On demande le remplacement du service
						setAttribute(SERVICE_RESOLUTION_NEED, new ServiceResolutionQuery() {							
							@Override
							public void setService(AbstractService service) {
								Log.i(Collaboration.class.getName()+"#onPropertyChange", "Setting service");
								cleanResource(r);								
								list.set(index, service);
								//On remplace le schema dans le service
								injectService(schemaInstance, serviceName, service);
								//On reverifie la collaboration
								checkCollaboration(schemaInstance);
								//On rajoute des tags au service pour pouvoir le tracer
								if(!service.hasAttribute(COLLABORATION_INSTANCE_TAG))
									service.declareAttribute(COLLABORATION_INSTANCE_TAG, String.class, false,false);
								if(!service.hasAttribute(COLLABORATION_INDEX_TAG))
									service.declareAttribute(COLLABORATION_INDEX_TAG, Integer.class, false,false);
								if(!service.hasAttribute(COLLABORATION_SCHEMA_INSTANCE))
									service.declareAttribute(COLLABORATION_SCHEMA_INSTANCE,CollaborationSchema.class,false,false);
								service.setAttribute(COLLABORATION_INSTANCE_TAG,instanceId);
								service.setAttribute(COLLABORATION_INDEX_TAG, (Integer)index);
								service.setAttribute(COLLABORATION_SCHEMA_INSTANCE, schemaInstance);
								//On renegocie
								schemaInstance.renegociate();
								//On rajoute le listener
								service.addListener(Collaboration.this);
							}
							
							@Override
							public void notFound() { 								
								Log.i(Collaboration.class.getName()+"#onPropertyChange", "Service substitute for %s[%s] not found",r.getClass().getName(),r.rid);
							}
							
							@Override
							public Description getDescription() {
								return description;
							}
							
						});
					}
				}
			}
		}
	}
	
}
